package klass.parse.builder;


import klass.parse.domain.*;
import klass.parse.domain.attribute.Attribute;
import klass.parse.domain.constant.*;
import klass.parse.domain.constant.factory.*;

import java.io.InputStream;
import java.util.*;

import static klass.parse.util.ByteUtils.*;
import static klass.parse.util.InputStreamUtils.*;

public class ClassInfoBuilder {

    String path;
    private InputStream inputStream;
    private ClassInfo classInfo;

    private static Map<Byte, ConstantFactory> constantFactoryMap;

    static {
        constantFactoryMap = new HashMap<Byte, ConstantFactory>();
        constantFactoryMap.put((byte) 1, new ConstantUtf8Factory());
        constantFactoryMap.put((byte) 3, new ConstantIntegerFactory());
        constantFactoryMap.put((byte) 7, new ConstantClassFactory());
        constantFactoryMap.put((byte) 10, new ConstantMethodRefFactory());
        constantFactoryMap.put((byte) 11, new ConstantInterfaceMethodRefFactory());
        constantFactoryMap.put((byte) 12, new ConstantNameAndTypeFactory());
    }

    public ClassInfoBuilder(String path) {
        this.path = path;
        this.inputStream = ClassInfoBuilder.class.getResourceAsStream(path);
    }

    public ClassInfo build() {
        classInfo = new ClassInfo();

        parseMagicNumber();
        parseMinorVersion();
        parseMajorVersion();
        parseConstantPool();
        parseAccessFlags();
        parseClass();
        parseSuperClass();
        parseInterfaces();
        parseFields();
        parseMethods();

        return classInfo;
    }

    private void parseMagicNumber() {
        byte[] bytes = next(inputStream, 4);
        if ("0xCAFFBABE".equals(byte2HexStr(bytes))) {
            throw new RuntimeException(path + " is not a class file!");
        }
    }

    private void parseMinorVersion() {
        MinorVersion minorVersion = new MinorVersion(nextShort(inputStream));
        classInfo.setMinorVersion(minorVersion);
    }

    private void parseMajorVersion() {
        MajorVersion majorVersion = new MajorVersion(nextShort(inputStream));
        classInfo.setMajorVersion(majorVersion);
    }

    private void parseConstantPool() {
        short count = nextShort(inputStream);
        for (int i = 0; i < count; i++) {
            if (i == 0) {
                classInfo.addConstant(null);
                continue;
            }
            Constant constant = parseConstant();
            assert constant != null;
            constant.setIndex(i);
            classInfo.addConstant(constant);
        }
        //todo 放到 ClassInfo 中
        postProcessReference(classInfo.getConstantPool());
    }

    private Constant parseConstant() {
        byte type = nextByte(inputStream);
        ConstantFactory factory = constantFactoryMap.get(type);
        if (factory == null) {
            throw new RuntimeException("Unresolvable constant type " + type);
        }
        return factory.create(inputStream);
    }

    private void parseAccessFlags() {
        AccessFlag accessFlag = new AccessFlag(nextShort(inputStream));
        classInfo.setAccessFlag(accessFlag);
    }

    private void parseClass() {
        short classIndex = nextShort(inputStream);
        classInfo.setClazz(classInfo.getConstant(classIndex).toString().replace("/", "."));
    }

    private void parseSuperClass() {
        short parentClassIndex = nextShort(inputStream);
        classInfo.setSuperClass(classInfo.getConstant(parentClassIndex).toString().replace("/", "."));
    }

    private void parseInterfaces() {
        short interfaceCount = nextShort(inputStream);
        for (int i = 0; i < interfaceCount; ++i) {
            short interfaceRef = nextShort(inputStream);
            String interfaceName = classInfo.getConstant(interfaceRef).toString();
            classInfo.getInterfaces().add(interfaceName);
        }
    }

    private void parseFields() {
        short fieldsCount = nextShort(inputStream);
        for (int i = 0; i < fieldsCount; i++) {
            FieldInfo field = new FieldInfo();
            field.setFlags(nextShort(inputStream));
            field.setName(classInfo.getConstant(nextShort(inputStream)).toString());
            field.setDescriptor(classInfo.getConstant(nextShort(inputStream)).toString());
            field.setAttributes(parseAttributes());
            classInfo.addField(field);
        }
    }

    private void parseMethods() {
        short methodCount = nextShort(inputStream);
        for (int i = 0; i < methodCount; i++) {
            MethodInfo methodInfo  = new MethodInfo();
            methodInfo.setFlags(nextShort(inputStream));
            methodInfo.setName(classInfo.getConstant(nextShort(inputStream)).toString());
            methodInfo.setDescriptor(classInfo.getConstant(nextShort(inputStream)).toString());
            methodInfo.setAttributes(parseAttributes());
            classInfo.addMethod(methodInfo);
        }
    }

    private List<Attribute> parseAttributes() {
        short attributeCount = nextShort(inputStream);
        List<Attribute> attributes = new ArrayList<Attribute>();
        for (int i = 0; i < attributeCount; i++) {
            attributes.add(parseAttribute());
        }
        return attributes;
    }

    private Attribute parseAttribute() {
        String attrName = classInfo.getConstant(nextShort(inputStream)).toString();
        int attrLength = nextInt(inputStream);
        byte[] bytes = next(inputStream, attrLength);
        Attribute attribute = new Attribute();
        attribute.setName(attrName);
        attribute.setBytes(bytes);
        return attribute;
    }

    private void postProcessReference(List<Constant> constants) {
        for (Constant constant : constants) {
            if (constant instanceof IPostProcessConstant) {
                IPostProcessConstant partialConstant = (IPostProcessConstant) constant;
                partialConstant.finish(constants);
            }
        }
    }

}
